use actix_service::forward_ready;

use futures::future::{ok, LocalBoxFuture, Ready};

use actix_web::{
    body::EitherBody,
    dev::{Service, ServiceRequest, ServiceResponse, Transform},
    http::Method,
    web::Data,
    Error, HttpMessage, ResponseError,
};

use crate::{
    common::{app, error::ApiError, types::R2D2Pool},
    config::{auth::AuthInfo, state::AppState},
    utils::{redis_key::RedisKey, redis_util::RedisUtil},
};

pub struct Authentication;

pub struct AuthenticationMiddleware<S> {
    service: S,
}

impl<S, B> Transform<S, ServiceRequest> for Authentication
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = Error;
    type Transform = AuthenticationMiddleware<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(AuthenticationMiddleware { service })
    }
}

impl<S, B> Service<ServiceRequest> for AuthenticationMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let authenticate_pass: bool = is_ignore_route(&req) || is_auth_route(&req).unwrap_or(false);
        match authenticate_pass {
            true => {
                let res = self.service.call(req);
                Box::pin(async move { res.await.map(ServiceResponse::map_into_left_body) })
            }
            false => {
                let (request, _pl) = req.into_parts();
                let response = ApiError::Unauthorized("未授权")
                    .error_response()
                    .map_into_right_body::<B>();
                Box::pin(async { Ok(ServiceResponse::new(request, response)) })
            }
        }
    }
}

fn is_ignore_route(req: &ServiceRequest) -> bool {
    Method::OPTIONS == req.method()
        || app::IGNORE_ROUTES
            .iter()
            .any(|&route| req.path().starts_with(route))
        || req.path().starts_with("/swagger-ui")
        || req.path().starts_with("/api-docs")
}

fn is_auth_route(req: &ServiceRequest) -> Option<bool> {
    let path = req.path();
    let state: &Data<AppState> = req.app_data::<Data<AppState>>()?;

    let authorization = req.headers().get(app::AUTHORIZATION)?;
    let header_token = authorization.to_str().ok()?;
    let header_token = header_token.strip_prefix("Bearer ")?.to_string();

    uuid::Uuid::parse_str(&header_token).ok()?;
    let auth_token_key = RedisKey::auth_token_key(&header_token);
    let auth_token_str = RedisUtil::get(&state.redis, &auth_token_key).ok()??;
    let auth_info: AuthInfo = serde_json::from_str(&auth_token_str).ok()?;
    let user_id_str = auth_info.user_id.to_string();

    let auth_path_json = RedisUtil::hget(&state.redis, RedisKey::PATH, path).ok()?;
    if let Some(auth_path_json) = auth_path_json {
        if !auth_path_json.is_empty() {
            let path_role_ids: Vec<i32> = serde_json::from_str(&auth_path_json).unwrap_or_default();
            let user_role_ids = &auth_info.role_ids;

            if !path_role_ids.is_empty()
                && !path_role_ids.iter().any(|id| user_role_ids.contains(id))
            {
                return Some(false);
            }
            renew_token(&state.redis, user_id_str, auth_token_key, auth_token_str);
        }
    }

    req.extensions_mut().insert(auth_info);
    Some(true)
}

fn renew_token(
    redis: &R2D2Pool,
    user_id_str: String,
    auth_token_key: String,
    auth_token_str: String,
) {
    let redis = redis.clone();
    actix_web::rt::spawn(async move {
        let ttl = RedisUtil::ttl(&redis, &auth_token_key).unwrap_or(0);
        if ttl > 0 && ttl < 3600 {
            let auth_user_key = RedisKey::auth_user_key(&user_id_str);
            let _ = RedisUtil::set(&redis, &auth_token_key, &auth_token_str, 7200);
            let _ = RedisUtil::set(&redis, &auth_user_key, &auth_token_key, 7200);
        }
    });
}
